#pragma once

#include "Layer.h"
#include "TLVData.h"

/// @file

/// @namespace pcpp
/// @brief The main namespace for the PcapPlusPlus lib
namespace pcpp
{
	/// @struct radius_header
	/// Represents a RADIUS protocol header
#pragma pack(push, 1)
	struct radius_header
	{
		/// RADIUS message code
		uint8_t code;
		/// RADIUS message ID
		uint8_t id;
		/// RADIUS message length
		uint16_t length;
		/// Used to authenticate the reply from the RADIUS server and to encrypt passwords
		uint8_t authenticator[16];
	};
#pragma pack(pop)
	static_assert(sizeof(radius_header) == 20, "radius_header size is not 20 bytes");

	/// @class RadiusAttribute
	/// A wrapper class for RADIUS attributes. This class does not create or modify RADIUS attribute records, but rather
	/// serves as a wrapper and provides useful methods for retrieving data from them
	class RadiusAttribute : public TLVRecord<uint8_t, uint8_t>
	{
	public:
		/// A c'tor for this class that gets a pointer to the attribute raw data (byte array)
		/// @param[in] attrRawData A pointer to the attribute raw data
		explicit RadiusAttribute(uint8_t* attrRawData) : TLVRecord(attrRawData)
		{}

		/// A d'tor for this class, currently does nothing
		~RadiusAttribute() override = default;

		// implement abstract methods

		size_t getTotalSize() const override
		{
			if (m_Data == nullptr)
				return 0;

			return static_cast<size_t>(m_Data->recordLen);
		}

		size_t getDataSize() const override
		{
			if (m_Data == nullptr)
				return 0;

			return static_cast<size_t>(m_Data->recordLen) - 2 * sizeof(uint8_t);
		}
	};

	/// @class RadiusAttributeBuilder
	/// A class for building RADIUS attributes. This builder receives the attribute parameters in its c'tor,
	/// builds the RADIUS attribute raw buffer and provides a build() method to get a RadiusAttribute object out of it
	class RadiusAttributeBuilder : public TLVRecordBuilder
	{
	public:
		/// A c'tor for building RADIUS attributes which their value is a byte array. The RadiusAttribute object can
		/// later be retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue A buffer containing the attribute value. This buffer is read-only and isn't modified in
		/// any way
		/// @param[in] attrValueLen Attribute value length in bytes
		RadiusAttributeBuilder(uint8_t attrType, const uint8_t* attrValue, uint8_t attrValueLen)
		    : TLVRecordBuilder(attrType, attrValue, attrValueLen)
		{}

		/// A c'tor for building RADIUS attributes which have a 1-byte value. The RadiusAttribute object can later be
		/// retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue A 1-byte attribute value
		RadiusAttributeBuilder(uint8_t attrType, uint8_t attrValue) : TLVRecordBuilder(attrType, attrValue)
		{}

		/// A c'tor for building RADIUS attributes which have a 2-byte value. The RadiusAttribute object can later be
		/// retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue A 2-byte attribute value
		RadiusAttributeBuilder(uint8_t attrType, uint16_t attrValue) : TLVRecordBuilder(attrType, attrValue)
		{}

		/// A c'tor for building RADIUS attributes which have a 4-byte value. The RadiusAttribute object can later be
		/// retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue A 4-byte attribute value
		RadiusAttributeBuilder(uint8_t attrType, uint32_t attrValue) : TLVRecordBuilder(attrType, attrValue)
		{}

		/// A c'tor for building RADIUS attributes which have an IPv4Address value. The RadiusAttribute object can later
		/// be retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue The IPv4 address attribute value
		RadiusAttributeBuilder(uint8_t attrType, const IPv4Address& attrValue) : TLVRecordBuilder(attrType, attrValue)
		{}

		/// A c'tor for building RADIUS attributes which have a string value. The RadiusAttribute object can later be
		/// retrieved by calling build()
		/// @param[in] attrType RADIUS attribute type
		/// @param[in] attrValue The string attribute value
		RadiusAttributeBuilder(uint8_t attrType, const std::string& attrValue) : TLVRecordBuilder(attrType, attrValue)
		{}

		/// A copy c'tor which copies all the data from another instance of RadiusAttributeBuilder
		/// @param[in] other The instance to copy from
		RadiusAttributeBuilder(const RadiusAttributeBuilder& other) : TLVRecordBuilder(other)
		{}

		/// Assignment operator that copies all data from another instance of RadiusAttributeBuilder
		/// @param[in] other The instance to assign from
		RadiusAttributeBuilder& operator=(const RadiusAttributeBuilder& other)
		{
			TLVRecordBuilder::operator=(other);
			return *this;
		}

		/// Build the RadiusAttribute object out of the parameters defined in the c'tor
		/// @return The RadiusAttribute object
		RadiusAttribute build() const;
	};

	/// @class RadiusLayer
	/// Represents a RADIUS (Remote Authentication Dial-In User Service) protocol layer
	class RadiusLayer : public Layer
	{
	private:
		TLVRecordReader<RadiusAttribute> m_AttributeReader;

		uint8_t* getAttributesBasePtr() const
		{
			return m_Data + sizeof(radius_header);
		}

		RadiusAttribute addAttrAt(const RadiusAttributeBuilder& attrBuilder, int offset);

	public:
		/// A constructor that creates the layer from an existing packet raw data
		/// @param[in] data A pointer to the raw data
		/// @param[in] dataLen Size of the data in bytes
		/// @param[in] prevLayer A pointer to the previous layer
		/// @param[in] packet A pointer to the Packet instance where layer will be stored in
		RadiusLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet)
		    : Layer(data, dataLen, prevLayer, packet, Radius)
		{}

		/// A constructor that creates a new layer from scratch
		/// @param[in] code The RADIUS message code
		/// @param[in] id The RADIUS message ID
		/// @param[in] authenticator A pointer to a byte array containing the authenticator value
		/// @param[in] authenticatorArrSize The authenticator byte array size. A valid size of the authenticator field
		/// is 16 bytes. If the provided size is less than that then the byte array will be copied to the packet but the
		/// missing bytes will stay zero. If the size is more than 16 bytes, only the first 16 bytes will be copied to
		/// the packet
		RadiusLayer(uint8_t code, uint8_t id, const uint8_t* authenticator, uint8_t authenticatorArrSize);

		/// A constructor that creates a new layer from scratch
		/// @param[in] code The RADIUS message code
		/// @param[in] id The RADIUS message ID
		/// @param[in] authenticator A hex string representing the authenticator value. A valid size of the
		/// authenticator field is 16 bytes. If the hex string represents an array that is smaller than this then the
		/// missing bytes in the packet's authenticator field will stay zero. If the hex string represents an array that
		/// is larger than 16 bytes, only the first 16 bytes will be copied to the packet
		RadiusLayer(uint8_t code, uint8_t id, const std::string& authenticator);

		/// A d'tor for this layer, currently does nothing
		~RadiusLayer() override = default;

		/// Get a pointer to the RADIUS header. Notice this points directly to the data, so every change will change the
		/// actual packet data
		/// @return A pointer to the radius_header object
		radius_header* getRadiusHeader() const
		{
			return reinterpret_cast<radius_header*>(m_Data);
		}

		/// @return A hex string representation of the radius_header#authenticator byte array value
		std::string getAuthenticatorValue() const;

		/// Setter for radius_header#authenticator
		/// @param[in] authValue A hex string representing the requested authenticator value
		void setAuthenticatorValue(const std::string& authValue);

		/// A static method that returns the RADIUS message string for a give message code. For example: the string
		/// "Access-Request" will be returned for code 1
		/// @param[in] radiusMessageCode RADIUS message code
		/// @return RADIUS message string
		static std::string getRadiusMessageString(uint8_t radiusMessageCode);

		/// @return The first RADIUS attribute in the packet. If there are no attributes the returned value will contain
		/// a logical null (RadiusAttribute#isNull() == true)
		RadiusAttribute getFirstAttribute() const;

		/// Get the RADIUS attribute that comes after a given attribute. If the given attribute was the last one, the
		/// returned value will contain a logical null (RadiusAttribute#isNull() == true)
		/// @param[in] attr A given attribute
		/// @return A RadiusAttribute object containing the attribute data that comes next, or logical null if the
		/// given attribute: (1) was the last one; (2) contains a logical null or (3) doesn't belong to this packet
		RadiusAttribute getNextAttribute(RadiusAttribute& attr) const;

		/// Get a RADIUS attribute by attribute type
		/// @param[in] attrType RADIUS attribute type
		/// @return A RadiusAttribute object containing the first attribute data that matches this type, or logical
		/// null (RadiusAttribute#isNull() == true) if no such attribute found
		RadiusAttribute getAttribute(uint8_t attrType) const;

		/// @return The number of RADIUS attributes in the packet
		size_t getAttributeCount() const;

		/// Add a new RADIUS attribute at the end of the layer
		/// @param[in] attrBuilder A RadiusAttributeBuilder object that contains the requested attribute data to add
		/// @return A RadiusAttribute object containing the newly added RADIUS attribute data or logical null
		/// (RadiusAttribute#isNull() == true) if addition failed
		RadiusAttribute addAttribute(const RadiusAttributeBuilder& attrBuilder);

		/// Add a new RADIUS attribute after an existing one
		/// @param[in] attrBuilder A RadiusAttributeBuilder object that contains the requested attribute data to add
		/// @param[in] prevAttrType The RADIUS attribute which the newly added attribute will come after
		/// @return A RadiusAttribute object containing the newly added RADIUS attribute data or logical null
		/// (RadiusAttribute#isNull() == true) if addition failed
		RadiusAttribute addAttributeAfter(const RadiusAttributeBuilder& attrBuilder, uint8_t prevAttrType);

		/// Remove an existing RADIUS attribute from the layer
		/// @param[in] attrType The RADIUS attribute type to remove
		/// @return True if the RADIUS attribute was successfully removed or false if type wasn't found or if removal
		/// failed
		bool removeAttribute(uint8_t attrType);

		/// Remove all RADIUS attributes in this layer
		/// @return True if all attributes were successfully removed or false if removal failed for some reason
		bool removeAllAttributes();

		/// The static method makes validation of UDP data
		/// @param[in] udpData The pointer to the UDP payload data. It points to the first byte of RADIUS header.
		/// @param[in] udpDataLen The payload data size
		/// @return True if the data is valid and can represent the RADIUS packet
		static bool isDataValid(const uint8_t* udpData, size_t udpDataLen);

		/// A static method that checks whether the port is considered as RADIUS
		/// @param[in] port The port number to be checked
		static inline bool isRadiusPort(uint16_t port);

		// implement abstract methods

		/// @return The size written in radius_header#length
		size_t getHeaderLen() const override;

		/// Does nothing for this layer, RADIUS is always last
		void parseNextLayer() override
		{}

		/// Calculate and store the value of radius_header#length according to the layer size
		void computeCalculateFields() override;

		std::string toString() const override;

		OsiModelLayer getOsiModelLayer() const override
		{
			return OsiModelApplicationLayer;
		}
	};

	// implementation of inline methods

	bool RadiusLayer::isRadiusPort(uint16_t port)
	{
		switch (port)
		{
		case 1812:
		case 1813:
		case 3799:
			return true;
		default:
			return false;
		}
	}  // isRadiusPort

}  // namespace pcpp
